/*
  * Mirante Tecnologia
  * Copyright 2010, Mirante Informatica LTDA, 
  * and individual contributors as indicated by the @authors tag
  *
  * This is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
  * published by the Free Software Foundation; either version 2.1 of
  * the License, or (at your option) any later version.
  *
  * This software is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  * Lesser General Public License for more details.
  *
  * You should have received a copy of the GNU Lesser General Public
  * License along with this software; if not, write to the Free
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
package com.miranteinfo.seam.jsf.component.datamodel;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.ajax4jsf.model.SerializableDataModel;
import org.jboss.seam.Component;
import org.jboss.seam.contexts.Contexts;

import com.miranteinfo.seam.framework.dao.DaoSupport;
import com.miranteinfo.seam.hibernate.BaseEntity;
import com.miranteinfo.seam.hibernate.util.HibernateProxyExtractor;
import com.miranteinfo.seam.util.DtoSupport;

/**
 * Classe desenvolvida com a finalidade de tornar a lista paginada em banco
 * mais simples. Aceita como elemento da lista objetos do tipo {@link BaseEntity} e {@link DtoSupport}.
 * Recebe como parametro essencial o {@link SeamDataModelControl}, o qual ira definir os metodos
 * que tornarao a paginacao da lista possivel.
 * 
 * @author lucas lins
 * 
 */
public class SeamDataModel<T> extends SerializableDataModel{
	
	private static final long serialVersionUID = 1L;
	
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// ATTRIBUTES
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
	int currentFirstRow = -1;
	
	public List<T> listRow;
	private int actualPage;
	private Object currentId;
	private int rowIndex;
	private Map<Object, T> wrappedData = new HashMap<Object, T>();
	private List<Object> wrappedKeys;
	private Long rowCount;
	private boolean resetRowCount;
	
	private boolean wrappToDto;
	private Class<T> dtoClass;
	private Class<? extends BaseEntity> entityClass;
	
	private SeamDataModelControl dataModelControl;
	
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// CONSTRUCTOR
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	
	@SuppressWarnings("unchecked")
	public SeamDataModel(Class<T> clazz, SeamDataModelControl dataModelControl){
		if(clazz == null || dataModelControl == null){
			throw new RuntimeException("clazz and dataModelControl must be not null!");
		}
		
		this.dataModelControl = dataModelControl;
		
		Class<DtoSupport> dtoSupportClass = DtoSupport.class;
		Class<BaseEntity> baseEntityClass = BaseEntity.class;
		
		if(dtoSupportClass.isAssignableFrom(clazz)){
			this.wrappToDto = true;
			this.dtoClass = (Class<T>) clazz;
			
			for(Constructor constructor : this.dtoClass.getConstructors()){
				Class[] parameters = constructor.getParameterTypes();
				if(parameters.length == 1 && baseEntityClass.isAssignableFrom(parameters[0])){
					this.entityClass = (Class<? extends BaseEntity>) parameters[0];
					break;
				}
			}
			
		}else if(baseEntityClass.isAssignableFrom(clazz)){
			this.wrappToDto = false;
		}else{
			throw new RuntimeException("T must extends BaseEntity or DtoSupport");
		}
	}
	
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// DATA MODEL CUSTOM METHODS
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	/**
	 * Returns the wrapped list.
	 * @return list.
	 */
	public Collection<T> getWrappedList(){
		return wrappedData.values();
	}
	
	
	/**
	 * Invokes the count method and returns the total number of rows.
	 * @return count
	 */
	public Long getCount() {
		Object service = retrieveService(dataModelControl.getServiceName());
		return (Long)invokeAction(service, dataModelControl.getCountMethodName(), dataModelControl.getCountParams());
	}

	/**
	 * Searches and returns the List from the database by invoking
	 * the service method.
	 * 
	 * @param firstRow
	 * @param maxResults
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List<T> getList(Integer firstRow, Integer maxResults) {
		try{
			
			Object service = retrieveService(dataModelControl.getServiceName());
			
			Contexts.getEventContext().set(DaoSupport.DAO_SUPPORT_FIRST_RESULT_PARAM, firstRow);
			Contexts.getEventContext().set(DaoSupport.DAO_SUPPORT_MAX_RESULTS_PARAM, maxResults);
			
			this.actualPage = (firstRow / maxResults) + 1;
			
			if(wrappToDto){
				List list = (List)invokeAction(service, dataModelControl.getMethodName(), dataModelControl.getParams());
				List<T> dtoList = new ArrayList<T>();
				
				if(!list.isEmpty()){
					
					Class<DtoSupport> dtoSupportClass = DtoSupport.class;
					Class<BaseEntity> baseEntityClass = BaseEntity.class;
					
					Object obj = list.get(0);
					
					if(dtoSupportClass.isAssignableFrom(obj.getClass())){
						
						dtoList.addAll(list);
						
					}else if(baseEntityClass.isAssignableFrom(obj.getClass())){
						
						Constructor<T> constructor = dtoClass.getConstructor(entityClass);
						for(Object entity : list){
							T dto = constructor.newInstance(entity);
							dtoList.add(dto);
						}
						
					}else{
						throw new RuntimeException("Result of service method must be a List of DtoSupport or BaseEntity.");
					}
				}
				
				return dtoList;
			}else{
				return (List<T>)invokeAction(service, dataModelControl.getMethodName(), dataModelControl.getParams());
			}
			
		}catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Returns the service by looking for the service name
	 * provided in all Seam scopes.
	 *
	 * @param serviceName
	 * @return
	 */
	private Object retrieveService(String serviceName) {
		Object service = Component.getInstance(serviceName);
		if(service == null){
			throw new RuntimeException("SeamDataModelAction: Service not found!");
		}
		return service;
	}

	/**
	 * Responsible for invoking the service method based on the parameters supplied.
	 *
	 * @param service
	 * @param methodName
	 * @param params
	 * @param serviceAction
	 * @return
	 */
	private Object invokeAction(Object service, String methodName, Object[] params){
		
		Method method = null;
		int paramsLength = params.length;
		Method[] methods = service.getClass().getDeclaredMethods();
		
		METHODS_LOOP:
		for(Method m : methods){
			if(m.getName().equals(methodName) && m.getParameterTypes().length == paramsLength){
				
				int i = 0;
				for(Class<?> paramType : m.getParameterTypes()){
					Object param = params[i++];
					if(param == null) continue;
					if(!paramType.isAssignableFrom(HibernateProxyExtractor.extractClass(param))){
						continue METHODS_LOOP;
					}
				}
				
				method = m;
				break;
			}
		}

		if(method == null){
			throw new RuntimeException("SeamDataModelAction: method not found in service");
		}

		try {
			return method.invoke(service, params);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public int getActualPage() {
		return actualPage;
	}

	public void setActualPage(int actualPage) {
		this.actualPage = actualPage;
	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// DATA MODEL DEFAULT METHODS
	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	@SuppressWarnings("unchecked")
	@Override
	public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) throws IOException{
		int firstRow = ((SequenceRange) range).getFirstRow();
		
		if(currentFirstRow != firstRow){
			
			currentFirstRow = firstRow;
			
			int numberOfRows = ((SequenceRange) range).getRows();
			
			wrappedKeys = new ArrayList<Object>();
			wrappedData = new HashMap<Object, T>();
			
			this.resetRowCount = true;
			
			List<T> list = getList(firstRow, numberOfRows);
			if (list != null && !list.isEmpty()) {
				for (T t : list) {
					Object pk = null;
					if (t instanceof BaseEntity) {
						pk = ((BaseEntity) t).getId();
					} else {
						pk = ((DtoSupport) t).getId();
					}
					
					wrappedKeys.add(pk);
					wrappedData.put(pk, t);
					visitor.process(context, pk, argument);
				}
			}
			
		}else{
			for(Object pk : wrappedKeys) visitor.process(context, pk, argument);
		}
		
	}

    @Override
	public SerializableDataModel getSerializableModel(Range range){
		if (wrappedKeys != null) {
			return this;
		}
		return null;
	}

    @Override
    public void update(){}

	@Override
	public void setRowKey(Object key){
		this.currentId = key;
	}
	
	@Override
	public Object getRowKey () {
		return currentId;
	}

	@Override
	public int getRowCount(){
		if(rowCount == null){
			rowCount = this.getCount();
		}else if(resetRowCount){
			rowCount = this.getCount();
		}
		resetRowCount = false;
		return rowCount.intValue();
	}

    @Override
	public boolean isRowAvailable() {
		if (currentId == null) {
			return false;
		}

		if (wrappedKeys.contains(currentId)) {
			return true;
		}

		if (wrappedData.entrySet().contains(currentId)) {
			return true;
		}

		if (hasById(currentId)) {
			return true;
		}

		return false;
	}

    public boolean hasById(Object id){
		if (listRow != null) {
			for (T row : listRow){
				Object rowId = getId(row);
				if (rowId.equals(id)){
					return true;
				}
			}
		}
		return false;
	}

    @SuppressWarnings("unchecked")
	public Object getId(T row){
		if (row instanceof BaseEntity) {
			return ((BaseEntity)row).getId();
		} else {
			return ((DtoSupport)row).getId();
		}
	}

	@Override
	public T getRowData() {
		if (currentId == null) {
			return null;
		} else {
			return wrappedData.get(currentId);
		}
	}
	
	@Override
	public int getRowIndex () {
		return this.rowIndex;
	}

	@Override
	public void setRowIndex (int rowIndex) {
		this.rowIndex = rowIndex;
	}
	
	// None of these methods are used anymore.
	
	@Override
	public Object getWrappedData () {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setWrappedData (Object data) {
		throw new UnsupportedOperationException();
	}
}
